# Copyright (c) 2018 Presto Labs Pte. Ltd.
# Author: jshin

import logging
import collections
import os

from coin.base.timestamp import get_timestamp
from coin.exchange.base.order_gateway import OrderSide
from coin.proto.coin_executor_pb2 import AggressiveExecutorConfig
from coin.strategy.executor.aggressive.interface import (AggressiveExecutorInterface)
from coin.strategy.executor.util.cancel_executor import CancelExecutor
from coin.exchange.util.cooldown_limit import CoolDownLimiter
from coin.strategy.executor.util.last_fire_timestamp import LastFireTimestamp
from coin.strategy.executor.util.last_order_state import LastOrderState
from coin.strategy.executor.util.order_adjuster import ProductInfoAdjuster
from coin.strategy.executor.util.config_adjuster import ExecutorConfigAdjuster
from coin.strategy.executor.util.working_order_state import (WorkingOrderStateGenerator)


class AggressiveExecutor(AggressiveExecutorInterface):
  ProductType = None
  OGType = None
  Traits = None

  def __init__(
      self,
      config,
      order_gateway,
      *,
      logger=None,
      latency_recorder=None,
      nbatch_loser_limit=10):
    assert isinstance(order_gateway, self.OGType)
    assert type(config) == AggressiveExecutorConfig, type(config)
    self._logger = logger or logging.getLogger('AggressiveExecutor')
    self._latency_recorder = latency_recorder
    self._config2 = ExecutorConfigAdjuster(config)
    self._product = self.ProductType.FromStr(self.config.symbol)
    self._product_adjuster = ProductInfoAdjuster(self._product)
    self._og = order_gateway
    self._og.set_latency_recorder(self._latency_recorder)
    self._traits = self.Traits(self._config2, order_gateway, self._product)

    self._logger.info('product: %s', self._product)
    self._logger.info("executor config:\n%s", self.config)

    self._position = None
    self._wo = WorkingOrderStateGenerator(product=self._product,
                                          og=self._og,
                                          sell_order_side=self._traits.SellOrderSide,
                                          buy_order_side=self._traits.BuyOrderSide)
    cooldown_limiter = CoolDownLimiter(0.5e9)
    self._cancel_executor = CancelExecutor(self._wo, cooldown_limiter)
    self._lo = LastOrderState(self.config.order_update_period, 0)
    self._lfiret = LastFireTimestamp(self.config)
    self._reset_action()
    self._prev_buy_code = None
    self._prev_sell_code = None

    self._prev_order_side = None
    self._prev_order_price = None
    self._losequeue = collections.deque()
    self._losecnt = 0
    self._nbatch_loser_limit = nbatch_loser_limit

  @property
  def config(self):
    # config = constant config
    # config2 = adjustable config
    return self._config2.config

  @property
  def config2(self):
    # config = constant config
    # config2 = adjustable config
    return self._config2

  @property
  def product(self):
    return self._product

  @property
  def og(self):
    return self._og

  @property
  def logger(self):
    return self._logger

  def set_config(self, config):
    assert type(config) == AggressiveExecutorConfig, type(config)
    self._config2.set_config(config)

  def query_unbiased_position(self):
    return self._traits._get_position(self._product)
  
  def set_close_only(self, mode):
    self._config2.set_close_only(mode)

  def _reset_action(self):
    self._cancel_executor.clear()
    self._fire_sell = False
    self._fire_buy = False

  def _update_state(self):
    self._position = self._traits._get_position(self._product)
    self._wo._update_order_state()

  def submit_agg_order_impl(self, *, order_side, order_price, order_qty):
    timestamp = get_timestamp()

    self._update_state()
    self._reset_action()

    if order_side == OrderSide.BUY and self._prev_order_side == OrderSide.SELL:
      # buy high sell low = lose
      loser = order_price > self._prev_order_price
      self._losequeue.append(loser)
      self._losecnt += loser
    elif order_side == OrderSide.SELL and self._prev_order_side == OrderSide.BUY:
      # sell low buy high = lose
      loser = order_price < self._prev_order_price
      self._losequeue.append(loser)
      self._losecnt += loser

    if len(self._losequeue) > self._nbatch_loser_limit:
      self._losecnt -= self._losequeue[0]
      self._losequeue.popleft()

    self._logger.info(f"losercount: {self._losecnt} / {len(self._losequeue)}")
    if self._losecnt >= self._nbatch_loser_limit:
      self._logger.error("your strategy runs charity business. killed.")
      os._exit(1)

    self._prev_order_side = order_side
    self._prev_order_price = order_price

    if order_side == OrderSide.BUY:
      return self._try_aggression_buy(timestamp=timestamp, buy_price=order_price, buy_qty=order_qty)
    elif order_side == OrderSide.SELL:
      return self._try_aggression_sell(timestamp=timestamp,
                                       sell_price=order_price,
                                       sell_qty=order_qty)
    else:
      raise ValueError(order_side)
      return False

  def manage_agg_orders_impl(self):
    timestamp = get_timestamp()

    self._update_state()
    self._reset_action()
    self._manage_risk(timestamp)

    self._cancel_executor.remove_duplicates()
    if self._cancel_executor.has_cancel():
      self._traits._cancel_orders(self._cancel_executor.get_cancel_list())

  def cancel_all_existing_buy(self):
    self._cancel_executor.cancel_buy_working_orders()
    self._cancel_executor.remove_duplicates()
    if self._cancel_executor.has_cancel():
      self._traits._cancel_orders(self._cancel_executor.get_cancel_list())

  def cancel_all_existing_sell(self):
    self._cancel_executor.cancel_sell_working_orders()
    self._cancel_executor.remove_duplicates()
    if self._cancel_executor.has_cancel():
      self._traits._cancel_orders(self._cancel_executor.get_cancel_list())

  def _manage_risk(self, timestamp):
    max_qty = self.config2.lot_size * self.config.max_working_cnt
    self._cancel_executor.cancel_buy_with_risk(self._position, self.config2.max_pos, max_qty)
    self._cancel_executor.cancel_sell_with_risk(self._position, self.config2.min_pos, max_qty)
    self._cancel_executor.cancel_old_orders(timestamp - self.config.max_posting_period)

  def _update_buy_order(self, timestamp, buy_side, buy_price, buy_qty):
    ##########################################################################
    # check in the order of:
    # input check
    # pos range check
    # inflight order check
    # similarity check
    # throttle

    # TODO: add dry_run
    dry_run = False

    ##########################################################################
    # input check
    logging.info("update_buy_order: buy_qty:%s, buy_price:%s", buy_qty, buy_price)
    logging.info("lot_size:%s, max_pos:%s, max_working_cnt:%s", self.config2.lot_size, self.config2.max_pos, self.config.max_working_cnt)
    logging.info("position:%s, buy_potential:%s", self._position, self._wo._buy_potential)

    # if timestamp is wrong, do not fire.
    if timestamp is None:
      return 17

    # if side is wrong, do not fire.
    if buy_side != OrderSide.BUY:
      return 13

    # if price is wrong, do not fire.
    if buy_price is None:
      return 12

    # if qty is wrong, do not fire.
    if (buy_qty is None or buy_qty <= 0):
      return 5

    # if qty is wrong, do not fire.
    if (buy_qty > self.config2.lot_size * 10):
      return 15

    ##########################################################################
    # pos range check

    # If current position is too big, do not fire.
    if (self._position > self.config2.max_pos):
      return 1

    # If current position is too big, do not fire.
    if (self._position + buy_qty > self.config2.max_pos):
      return 1

    # If potential position is too big, do not fire.
    if (self._wo._buy_potential + self._position + buy_qty > self.config2.max_pos):
      return 2

    # If buy potential is too big, do not fire.
    if (self._wo._buy_potential > self.config2.lot_size * self.config.max_working_cnt):
      return 7

    ##########################################################################
    # force_fire

    if self.config.force_fire:
      if not dry_run:
        self._fire_buy = True
      return 0

    ##########################################################################
    # inflight order check

    # If number of inflight orders are too much, do not fire.
    if self.config.allow_fast_order:
      if self._wo.get_buy_unack_count > self.config.max_inflight_cnt * 10:
        return 8

      if self._wo.get_buy_unack_qty > self.config2.lot_size * self.config.max_inflight_cnt:
        return 16
    else:
      if self._wo.get_buy_unack_count > self.config.max_inflight_cnt:
        return 8

    ##########################################################################
    # similarity check

    # If last price is similar, do not fire.
    if not self._lo._is_buy_last_order_far(timestamp, buy_price):
      return 9

    # If there is an working order that its price is same to buy_price,
    # keep it and do not fire.
    for order in self._wo._buy_orders:
      if abs(order.price - buy_price) <= 0:
        return 3

    ##########################################################################
    # throttle

    # If within order_update_period, do not fire.
    if self._lfiret._within_buy_update_period(timestamp):
      return 11

    # If within fill_cooldown_period, do not fire.
    if self._lfiret._within_buy_sell_fill_cooldown_period(timestamp):
      return 14

    # If rate limit is reached, do not fire.
    # if not self._is_buy_rate_limited():
    #  return 6

    ##########################################################################
    # fire

    if not dry_run:
      self._fire_buy = True
    return 0

  def _update_sell_order(self, timestamp, sell_side, sell_price, sell_qty):
    ##########################################################################
    # check in the order of:
    # input check
    # pos range check
    # inflight order check
    # similarity check
    # throttle

    # TODO: add dry_run
    dry_run = False

    ##########################################################################
    # input check
    logging.info("update_sell_order: sell_qty:%s, sell_price:%s", sell_qty, sell_price)
    logging.info("lot_size:%s, min_pos:%s, max_working_cnt:%s", self.config2.lot_size, self.config2.min_pos, self.config.max_working_cnt)
    logging.info("position:%s, sell_potential:%s", self._position, self._wo._sell_potential)
    # if timestamp is wrong, do not fire.
    if timestamp is None:
      return 17

    # if side is wrong, do not fire.
    if sell_side != OrderSide.SELL:
      return 13

    # if price is wrong, do not fire.
    if sell_price is None:
      return 12

    # if qty is wrong, do not fire.
    if (sell_qty is None or sell_qty <= 0):
      return 5

    # if qty is wrong, do not fire.
    if (sell_qty > self.config2.lot_size * 10):
      return 15

    ##########################################################################
    # pos range check

    # If current position is too small, do not fire.
    if (self._position < self.config2.min_pos):
      return 1

    # If current position is too small, do not fire.
    if (self._position - sell_qty < self.config2.min_pos):
      return 2

    # If potential position is too small, do not fire.
    if (self._wo._sell_potential + self._position - sell_qty < self.config2.min_pos):
      return 2

    # If sell potential is too small, do not fire.
    if (-1 * self._wo._sell_potential > self.config2.lot_size * self.config.max_working_cnt):
      return 7

    ##########################################################################
    # force_fire

    if self.config.force_fire:
      if not dry_run:
        self._fire_sell = True
      return 0

    ##########################################################################
    # inflight order check

    # If number of inflight orders are too much, do not fire.
    if self.config.allow_fast_order:
      if self._wo.get_sell_unack_count > self.config.max_inflight_cnt * 10:
        return 8

      if self._wo.get_sell_unack_qty > self.config2.lot_size * self.config.max_inflight_cnt:
        return 16
    else:
      if self._wo.get_sell_unack_count > self.config.max_inflight_cnt:
        return 8

    ##########################################################################
    # similarity check

    # If last price is similar, do not fire.
    if not self._lo._is_sell_last_order_far(timestamp, sell_price):
      return 9

    # If there is an order that its price is same to sell_price,
    # keep it and do not fire.
    for order in self._wo._sell_orders:
      if abs(order.price - sell_price) <= 0:
        return 3

    ##########################################################################
    # throttle

    # If within order_update_period, do not fire.
    if self._lfiret._within_sell_update_period(timestamp):
      return 11

    # If within fill_cooldown_period, do not fire.
    if self._lfiret._within_buy_sell_fill_cooldown_period(timestamp):
      return 14

    # If rate limit is reached, do not fire.
    # if not self._is_sell_rate_limited():
    #  return 6

    ##########################################################################
    # fire

    if not dry_run:
      self._fire_sell = True
    return 0

  def _try_aggression_buy(self, *, timestamp, buy_price=None, buy_qty=None):
    logging.info("_try_aggression_buy: %s, %s", buy_price, buy_qty)
    # determine order side
    order_side = OrderSide.BUY

    # determine order price
    order_price = buy_price or self._book.ask0()
    order_price = self._product_adjuster.floor_price(order_price)

    # determine order size
    order_qty = buy_qty or self.config2.lot_size
    if self._position is not None and self._config2.close_only:
      pos = self._position - self.config2.reserve
      if pos >= 0:
        order_qty = 0
      else:  # pos < 0
        order_qty = min(abs(pos), order_qty)
    if self._position is not None:
      order_qty = min(order_qty, max(0, (self.config2.max_pos - self._position)))
    if order_qty < self.config2.lot_size * self.config.min_lot_size_ratio:
      order_qty = 0
    if order_qty > self.config2.lot_size * self.config.max_lot_size_ratio:
      order_qty = self.config2.lot_size * self.config.max_lot_size_ratio
    if order_qty > 0:
      order_qty = self._product_adjuster.floor_qty(order_qty)

    # fire
    self._prev_buy_code = self._update_buy_order(timestamp, order_side, order_price, order_qty)
    logging.info("_try_aggression_buy - return code: %s", self._prev_buy_code)
    if self._fire_buy and order_qty > 0:
      self._lfiret.set_last_buy_fire(timestamp)
      self._traits._submit_order(self._product, order_price, order_qty, OrderSide.BUY)

    # Cancel orders for risk violation
    self._manage_risk(timestamp)

    ret = False

    if self._fire_buy:
      self._lfiret.set_last_buy_fire(timestamp)
      ret = True

    return ret

  def _try_aggression_sell(self, *, timestamp, sell_price=None, sell_qty=None):
    logging.info("_try_aggression_sell: %s, %s", sell_price, sell_qty)
    # determine order side
    order_side = OrderSide.SELL

    # determine order price
    order_price = sell_price or self._book.bid0()
    order_price = self._product_adjuster.ceil_price(order_price)

    # determine order size
    order_qty = sell_qty or self.config2.lot_size
    if self._position is not None and self._config2.close_only:
      pos = self._position - self.config2.reserve
      if pos <= 0:
        order_qty = 0
      else:  # pos > 0:
        order_qty = min(abs(pos), order_qty)
    if self._position is not None:
      order_qty = min(order_qty, max(0, (self._position - self.config2.min_pos)))
    if order_qty < self.config2.lot_size * self.config.min_lot_size_ratio:
      order_qty = 0
    if order_qty > self.config2.lot_size * self.config.max_lot_size_ratio:
      order_qty = self.config2.lot_size * self.config.max_lot_size_ratio
    if order_qty > 0:
      order_qty = self._product_adjuster.floor_qty(order_qty)

    # fire
    self._prev_sell_code = self._update_sell_order(timestamp, order_side, order_price, order_qty)
    logging.info("_try_aggression_sell - return code: %s", self._prev_sell_code)
    if self._fire_sell and order_qty > 0:
      self._lfiret.set_last_sell_fire(timestamp)
      self._traits._submit_order(self._product, order_price, order_qty, OrderSide.SELL)

    # Cancel orders for risk violation
    self._manage_risk(timestamp)

    if self._cancel_executor.has_cancel():
      self._traits._cancel_orders(self._cancel_executor.get_cancel_list())

    ret = False

    if self._fire_sell:
      self._lfiret.set_last_sell_fire(timestamp)
      ret = True

    return ret
